package com.tachoknight.adventure;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

/*
 * The purpose of this program is to transform the adventure data file to a SQL file
 * that we can then import into a SQLite3 database.
 * 
 * The SQLite3 database will be used for holding the game data for those
 * environments where we may not be able to get at a full-blown RDBMS.
 * 
 * Original Author: Ron Olson
 * Creation Date: 	3/7/2009
 * 
 */

/**
 * A custom exception for catching the possibility that the number of values
 * supplied doesn't match the number of columns this table expects.
 */
class ColumnValueMiscountException extends Exception {
	private static final long serialVersionUID = -5722098901506355011L;

	public ColumnValueMiscountException(List<Column> columns,
			List<String> values) {
		super(buildErrorMessage(columns, values));
	}

	private static String buildErrorMessage(List<Column> columns,
			List<String> values) {
		String message = "Column/Value count doesn't match!";

		for (Column col : columns) {
			message += "\n\tColumn: " + col.getName() + "(type is "
					+ col.getType() + ")";
		}

		for (String val : values) {
			message += "\n\tValue: " + val;
		}

		return message;
	}
}

/**
 * Defines both the column name and its type; this class is used in the list of
 * columns in the {@link Table} class.
 * 
 */
class Column {
	private String name;
	private String type;

	public Column(String name, String type) {
		this.name = name;
		this.type = type;
	}

	public String getName() {
		return name;
	}

	public String getType() {
		return type;
	}
}

/**
 * <tt>Table</tt> is a base class for holds the column information for a
 * particular table and also provides the functionality to create a table and
 * the insert statements.
 * 
 * The actual tables we use inherit from this class.
 */
abstract class Table {
	protected String tableName = "";
	protected List<Column> columns = new ArrayList<Column>();

	/**
	 * This method writes the SQL to create the table
	 * 
	 * @return The table's "create" statement
	 */
	public String writeCreateTableSQL() {
		String createSQL = "\n\ncreate table " + tableName;

		createSQL += " (";
		Integer columnCount = columns.size();
		Integer currentCol = 0;
		for (Column col : columns) {
			createSQL += col.getName();
			createSQL += " ";
			createSQL += col.getType();

			if (++currentCol < columnCount)
				createSQL += ",";
		}
		createSQL += ");\n";

		return createSQL;
	}

	/**
	 * Creates a single insert statement that contains the values passed to it.
	 * 
	 * @param The
	 *            list of values for the insert statement
	 * @return The "insert into" statement
	 * @throws ColumnValueMiscountException
	 */
	public String writeInsertSQL(List<String> values)
			throws ColumnValueMiscountException {
		if (values.size() != columns.size())
			throw new ColumnValueMiscountException(columns, values);

		String insertSQL = "insert into \"" + tableName + "\"";

		/* First the columns for the insert statement */
		insertSQL += " (";
		Integer columnCount = columns.size();
		Integer currentCol = 0;
		for (Column col : columns) {
			insertSQL += col.getName();
			if (++currentCol < columnCount)
				insertSQL += ",";
		}
		insertSQL += ")";

		/* Now the values */
		insertSQL += " values (";

		Integer currentVal = 0;
		for (String val : values) {
			insertSQL += val;
			if (++currentVal < columnCount)
				insertSQL += ",";
		}
		insertSQL += ")";

		insertSQL += ";\n";

		return insertSQL;
	}
}

class FullDescriptionTable extends Table {
	{
		super.tableName = "full_desc";

		super.columns.add(new Column("id", "int"));
		super.columns.add(new Column("desc", "varchar2(4000)"));
	}
}

class ShortDescriptionTable extends Table {
	{
		super.tableName = "short_desc";

		super.columns.add(new Column("id", "int"));
		super.columns.add(new Column("desc", "varchar2(4000)"));
	}
}

class Verbs extends Table {
	{
		super.tableName = "verbs";

		super.columns.add(new Column("id", "int"));
		super.columns.add(new Column("verb", "varchar2(4000)"));
	}

}

class Messages extends Table {
	{
		super.tableName = "messages";

		super.columns.add(new Column("id", "int"));
		super.columns.add(new Column("message", "varchar2(4000)"));
	}
}

class GameMap extends Table {
	{
		super.tableName = "map";

		super.columns.add(new Column("start", "int"));
		super.columns.add(new Column("end", "int"));
		super.columns.add(new Column("via", "int"));
	}
}

class State extends Table {
	{
		super.tableName = "state";

		super.columns.add(new Column("id", "int"));
		super.columns.add(new Column("message", "varchar2(4000)"));
	}
}

interface Parserible {
	public void parse(String line);

	public void dumpToFile(FileWriter sqlStream);
}

abstract class Parser implements Parserible {
	protected List<List<String>> insertStatements = new ArrayList<List<String>>();
	protected Table table = null;

	public Parser(Table sqlTable) {
		this.table = sqlTable;
	}

	abstract public void parse(String line);

	abstract public void finalizeParseLine();

	public void dumpToFile(FileWriter sqlStream) {
		finalizeParseLine();

		try {
			sqlStream.write(table.writeCreateTableSQL() + "\n");
			for (List<String> currentInsert : insertStatements) {
				sqlStream.write(table.writeInsertSQL(currentInsert));
			}

			sqlStream.flush();
		} catch (ColumnValueMiscountException cvme) {
			System.err.println("Got a column/value miscount exception: "
					+ cvme.getMessage());
		} catch (IOException ioe) {
			System.err.println("When dumping the data to the sql file, got: "
					+ ioe.getMessage());
		}
	}

	protected List<String> breakLineIntoBaseParts(String line) {
		List<String> baseParts = new ArrayList<String>();

		int firstSpacePos = line.indexOf(' ');
		baseParts.add(line.substring(0, firstSpacePos));
		baseParts.add(line.substring(firstSpacePos + 1));

		return baseParts;
	}
}

class TwoFieldParser extends Parser {
	private String currentDescriptionNum = "";
	private String currentDescription = "";

	private boolean doNotCombineLines = false;

	public TwoFieldParser(Table sqlTable) {
		super(sqlTable);
	}

	public TwoFieldParser(Table sqlTable, boolean doNotCombineLines) {
		super(sqlTable);

		this.doNotCombineLines = doNotCombineLines;
	}

	public void parse(String line) {
		/*
		 * In this block, we have two elements, the description number and the
		 * description text.
		 */
		List<String> baseParts = breakLineIntoBaseParts(line);

		if (this.doNotCombineLines == true) {
			currentDescriptionNum = baseParts.get(0);
			currentDescription = baseParts.get(1).trim() + "\n";

			finalizeParseLine();

			currentDescriptionNum = "";
			currentDescription = "";
		} else {
			if (currentDescriptionNum.compareTo(baseParts.get(0)) == 0) {
				currentDescription += (baseParts.get(1).trim() + "\n");
			} else {
				if (currentDescription.length() > 0) {
					finalizeParseLine();
				}

				currentDescriptionNum = baseParts.get(0);
				currentDescription = baseParts.get(1).trim() + "\n";
			}
		}
	}

	public void finalizeParseLine() {
		if (currentDescriptionNum.length() > 0
				&& currentDescription.length() > 0) {
			currentDescription = currentDescription.replaceAll("\"", "\"\"");
			List<String> newStatement = new ArrayList<String>();
			newStatement.add(currentDescriptionNum);
			newStatement.add("\"" + currentDescription + "\"");
			insertStatements.add(newStatement);
		}
	}
}

class MapParser extends Parser {
	String from = "";
	String to = "";
	String via = "";

	public MapParser(Table sqlTable) {
		super(sqlTable);
	}

	public void parse(String line) {
		/*
		 * Each line is broken down into from, to, and all the "vias"
		 */
		StringTokenizer tokenizer = new StringTokenizer(line);

		from = tokenizer.nextToken();
		to = tokenizer.nextToken();

		while (tokenizer.hasMoreTokens()) {
			via = tokenizer.nextToken();
			finalizeParseLine();
		}
	}

	public void finalizeParseLine() {
		List<String> newStatement = new ArrayList<String>();
		newStatement.add(from);
		newStatement.add(to);
		newStatement.add(via);
		insertStatements.add(newStatement);
	}
}

/**
 * This is the main class that does all the actual work.
 */
public class ConvertDataToSQLite {
	private final String ADVENT_DATA_FILE = "advdat31.dat";
	private final String ADVENT_SQL_FILE = "advent.sql";

	/* See addParsers for how this map is used */
	private Map<Integer, Parser> parsers = new HashMap<Integer, Parser>();
	private Parser currentParser = null;
	private Integer currentBlock = 0;

	/* This is our file that will contain all the create/insert statements */
	FileWriter sqlStream = null;

	/**
	 * Since this program does one thing, might as well do it in the constructor
	 */
	public ConvertDataToSQLite() {
		try {
			sqlStream = new FileWriter(ADVENT_SQL_FILE);

			addParsers();
			parseDataFile();
		} catch (IOException ioe) {
			System.err.println("Could not open " + ADVENT_SQL_FILE
					+ " for writing because" + ioe.getMessage());
		}
	}

	/**
	 * The file is broken up into several blocks, some of which look similar to
	 * others, but not every block works the same way. Thus we have different
	 * parsers based on the particular block (identified by the number)
	 */
	void addParsers() {
		/*
		 * This section is for those parts of the file that are basically in two
		 * parts: the id (or for Scores, the score) and the text.
		 */
		parsers.put(1, new TwoFieldParser(new FullDescriptionTable()));
		parsers.put(2, new TwoFieldParser(new ShortDescriptionTable()));
		parsers.put(4, new TwoFieldParser(new Verbs(), true));
		parsers.put(5, new TwoFieldParser(new State()));
		parsers.put(6, new TwoFieldParser(new Messages()));

		/*
		 * This section is for our special and custom blocks
		 */
		parsers.put(3, new MapParser(new GameMap()));
	}

	/**
	 * This is our "top-level" method that opens the data file and gets each
	 * line. We then send the line to {@link parseLine} which will figure out
	 * what to do with it.
	 */
	private void parseDataFile() {
		try {
			String line;

			FileReader frd = new FileReader(ADVENT_DATA_FILE);
			BufferedReader reader = new BufferedReader(frd);

			while ((line = reader.readLine()) != null) {
				parseLine(line);
			}
		} catch (IOException ioe) {
			System.err.println("Got an IOException when reading the file: "
					+ ioe.getMessage());
		}
	}

	private void parseLine(String line) {
		/*
		 * 0 = end of file
		 */
		if (line.compareTo("0") == 0) {
			/* We're done */
			return;
		}

		if (isNewBlock(line)) {
			/*
			 * We're going into a new block of the file, defined by
			 * this.currentBlock, so we use that to get the parser for that
			 * particular block.
			 */
			currentParser = parsers.get(currentBlock);
		} else if (isEndBlock(line)) {
			/*
			 * We hit the block terminator (-1), so now we can write out the
			 * parser's contents to the SQL file
			 */
			currentParser.dumpToFile(sqlStream);
			currentParser = null;
		} else {
			/*
			 * Some random line in a block
			 */
			if (currentParser != null)
				currentParser.parse(line);
		}
	}

	private boolean isNewBlock(String line) {
		/*
		 * A new block is a line that has a positive number and nothing else.
		 * How can we tell? Well, we know that there are a specific number of
		 * blocks (1 or 2 digit numbers) and they're positive.
		 * 
		 * So what we do is check for a line that has a length of two characters
		 * or less and the resulting string can be converted to a positive
		 * number.
		 */
		if (line.length() > 2)
			return false;

		/* Okay, let's see about converting the line to a number */
		try {
			Integer testNum = Integer.valueOf(line);
			if (testNum != -1) {
				currentBlock = testNum;
				return true;
			}

			return false;
		} catch (NumberFormatException nfe) {
			System.err.println("Could not convert " + line
					+ " to a number because: " + nfe.getMessage());
		}

		return false;
	}

	private boolean isEndBlock(String line) {
		/*
		 * The data file uses "-1" to indicate the end of a section. Sometimes a
		 * word is appended after the -1 so we want to make sure that we're
		 * explicitly testing for the -1
		 */
		return line.substring(0, 2).compareTo("-1") == 0;
	}

	/*
	 * Run the app :)
	 */
	public static void main(String[] args) {
		new ConvertDataToSQLite();
		System.out.println("All Done!");
	}
}
